﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Net;

namespace Wombat.Core.Services
{
    internal interface IObservableProxy
    {
        void OnError(Exception exception);
        void OnCompleted();
    }

    public class ObservableProxy<T> : IObservableProxy, IObservable<T>, IObserver<T> where T: ITagValue
    {
        private struct Unsubscriber : IDisposable
        {
            public Unsubscriber(ObservableProxy<T> creator, WeakReference handle)
            {
                _creator = creator;
                _handle = handle;
            }

            private readonly ObservableProxy<T> _creator;
            private readonly WeakReference _handle;

            public void Dispose()
            {
                if (!_handle.IsAlive)
                    return;
                _creator.RemoveObserver(_handle);
            }
        }

        private readonly List<WeakReference> _observers = new List<WeakReference>();
        private readonly object _cleanUpLock = new object();
        private DateTime _lastCleanUp = DateTime.UtcNow;
        private readonly TimeSpan _cleanUpDelay = TimeSpan.FromMinutes(1);

        private void RemoveObserver(WeakReference handle)
        {
            Contract.Assume(handle.IsAlive);

            var targetHandle = handle.Target;
            var idx = _observers.FindIndex(0, o => o.IsAlive && o.Target.Equals(targetHandle));
            if (idx >= 0)
            {
                lock (_cleanUpLock)
                {
                    _observers.RemoveAt(idx);
                }
            }
            ScheduleCleanUp();
        }

        private void ScheduleCleanUp()
        {
            if ((DateTime.UtcNow - _lastCleanUp) > _cleanUpDelay)
                CleanUp();
        }

        private void CleanUp()
        {
            lock (_cleanUpLock)
            {
                _observers.RemoveAll(o => !o.IsAlive);
                _lastCleanUp = DateTime.UtcNow;
            }
        }

        public IDisposable Subscribe(IObserver<T> observer)
        {
            var existingSubscriber = _observers.FirstOrDefault(o => o.IsAlive && o.Target == observer);
            var newReference = new WeakReference(observer);
            if (existingSubscriber == null)
                _observers.Add(newReference);
            return new Unsubscriber(this, newReference);
        }

        public void OnNext(T data)
        {
            var exceptions = default(List<Exception>);
            foreach (WeakReference t in _observers.Where(t => t.IsAlive))
            {
                try
                {
                    ((IObserver<T>) t.Target).OnNext(data);
                }
                catch (Exception exception)
                {
                    exceptions = exceptions ?? new List<Exception>();
                    exceptions.Add(exception);
                }
            }
            if (exceptions != null)
            {
                throw new AggregateException(string.Format("One or more subsciber failed to process data for tag {0} at time {1}.", data.TagId, data.Time),
                    exceptions);
            }
        }

        public void OnError(Exception exception)
        {
            foreach (var observer in _observers)
            {
                if (observer.IsAlive)
                {
                    ((IObserver<T>)observer.Target).OnError(exception);
                }
            }
        }

        public void OnCompleted()
        {
            foreach (var observer in _observers)
            {
                if (observer.IsAlive)
                {
                    ((IObserver<T>)observer.Target).OnCompleted();
                }
            }
        }
    }

}
